using System;
using System.Data;
using System.Globalization;

namespace BizElements.Core
{
	#region Documentation.
	/// <summary>
	/// Represents a parent/child relationship between two <see cref="IDbTable"/> objects.
	/// <seealso cref="RelationBucket"/>
	/// </summary>
	/// <example>
	/// The following examples creates a relation between Employees table (parent) and EmployeeTerritories table (child).
	/// <code>
	/// EmployeesMeta employees = new EmployeesMeta();
	/// EmployeeTerritoriesMeta empTerritories = new EmployeeTerritoriesMeta();
	/// DbRelation relation = new DbRelation(employees, empTerritories, empTerritories.EmployeeID);
	/// </code>
	/// </example>
	#endregion
	[Serializable]
	public sealed class DbRelation : ICloneable	
	{
		#region Members.

		/// <summary>
		/// Relation name.
		/// </summary>
		string name;

		/// <summary>
		/// Parent table.
		/// </summary>
		IDbTable parent;

		/// <summary>
		/// Parent table's primary key fields.
		/// </summary>
		IDbColumn[] parentPK;

		/// <summary>
		/// Child table.
		/// </summary>
		IDbTable child;

		/// <summary>
		/// Child table's foreing key fields which connect it with the parent table.
		/// </summary>
		IDbColumn[] childFK;

		#endregion

		#region Constructors.

		private DbRelation()
		{
		}

		/// <summary>Initializes a new instance of the DbRelation class.</summary>
		/// <remarks>Creates a relation between two tables.</remarks>
		/// <param name="parent">Parent/master table.</param>
		/// <param name="child">Child/data table.</param>
		/// <param name="childForeignKey">Child foreign key.</param>
		/// <param name="name">Relation name. If <b>null</b> then a default name is created.</param>
		public DbRelation(IDbTable parent, IDbTable child, IDbColumn childForeignKey, string name)
		{			
			Init(parent, parent.PrimaryKey.ToArray(), child, new IDbColumn[]{childForeignKey}, name);
		}

		/// <summary>Initializes a new instance of the DbRelation class.</summary>
		/// <remarks>Creates a relation between two tables with a default name.</remarks>
		/// <param name="parent">Parent/master table.</param>
		/// <param name="child">Child/data table.</param>
		/// <param name="childForeignKey">Child foreign key.</param>
		public DbRelation(IDbTable parent, IDbTable child, IDbColumn childForeignKey)
		{
			Init(parent, parent.PrimaryKey.ToArray(), child, new IDbColumn[]{childForeignKey}, null);
		}

        /// <summary>Initializes a new instance of the DbRelation class.</summary>
        /// <remarks>Creates a relation between two tables with a default name.</remarks>
        /// <param name="parent">Parent/master table.</param>
        /// <param name="parentPrimaryKey">Parent primary key.</param>
        /// <param name="child">Child/data table.</param>
        /// <param name="childForeignKey">Child foreign key.</param>
        public DbRelation(IDbTable parent, IDbColumn parentPrimaryKey, IDbTable child, IDbColumn childForeignKey)
        {
            Init(parent, new IDbColumn[]{parentPrimaryKey}, child, new IDbColumn[]{childForeignKey}, null);
        }

		/// <summary>Initializes a new instance of the DbRelation class.</summary>
		/// <remarks>Creates a relation between two tables with a default name.</remarks>
		/// <param name="parent">Parent/master table.</param>
		/// <param name="parentPrimaryKey">Parent primary key.</param>
		/// <param name="child">Child/data table.</param>
		/// <param name="childForeignKey">Child foreign key.</param>
		/// <param name="relationName">Relation name. If <b>null</b> then a default name is created.</param>
		public DbRelation(IDbTable parent, IDbColumn[] parentPrimaryKey, IDbTable child, IDbColumn[] childForeignKey, string relationName)
		{
            Init(parent, parentPrimaryKey, child, childForeignKey, relationName);
		}

        /// <summary>Initializes a new instance of the DbRelation class.</summary>
        /// <param name="relationConfig">Relation data.</param>
        public DbRelation(IDbRelationConfiguration relationConfig)
        {
            IDbTable parent = relationConfig.CreateParent();
            IDbTable child = relationConfig.CreateChild();
            Init(parent, DbRelationConfigurationUtil.GetParentPrimayKeyColumns(relationConfig, parent), child, DbRelationConfigurationUtil.GetChildForeignKeyColumns(relationConfig, child), null);
        }

        /// <summary>Initializes a new instance of the DbRelation class.</summary>
        /// <param name="relationConfig">Relation data.</param>
        /// <param name="parentAlias">Optional parent table alias.</param>
        /// <param name="childAlias">Optional child table alias.</param>
        public DbRelation(IDbRelationConfiguration relationConfig, string parentAlias, string childAlias)
        {
            IDbTable parent = relationConfig.CreateParent();
            if (!string.IsNullOrEmpty(parentAlias))
                parent = parent.Clone(parentAlias);

            IDbTable child = relationConfig.CreateChild();
            if (!string.IsNullOrEmpty(childAlias))
                child = child.Clone(childAlias);

            Init(parent, DbRelationConfigurationUtil.GetParentPrimayKeyColumns(relationConfig, parent), child, DbRelationConfigurationUtil.GetChildForeignKeyColumns(relationConfig, child), null);
        }

		/// <summary>
		/// Initializes object.
		/// </summary>
		/// <param name="parent">Parent/master table.</param>
		/// <param name="parentPrimaryKey">Parent primary key.</param>
		/// <param name="child">Child/data table.</param>
		/// <param name="childForeignKey">Child foreign key. The collection must exactly match the order and 
		/// the types of parent's primary key fields.</param>
		/// <param name="name">Relation name. If <b>null</b> then a default name is created.</param>
		private void Init(IDbTable parent, IDbColumn[] parentPrimaryKey, IDbTable child, IDbColumn[] childForeignKey, string name)
		{
			ValidateKeys(parentPrimaryKey, childForeignKey);

			this.parent = parent;
			this.parentPK = parentPrimaryKey;
			this.child = child;
			this.childFK = childForeignKey;
            this.name = name;
		}

		/// <summary>
		/// Generates a default name.
		/// </summary>
		/// <param name="parent">Parent/master table.</param>
		/// <param name="child">Child/data table.</param>
		/// <returns>Relation name.</returns>
		private static string GenerateName(IDbTable parent, IDbTable child)
		{
			return "FK_" + child.Alias + "_" + parent.Alias;
		}
	

		/// <summary>
		/// Validates whether the parent's primary key is compatible with the child's foreing key.
		/// Throws exception on error.
		/// </summary>
		/// <param name="parentPrimaryKey">Parant table's primary key.</param>
		/// <param name="childForeignKey">Child table's foreign key.</param>
		private static void ValidateKeys(IDbColumn[] parentPrimaryKey, IDbColumn[] childForeignKey)
		{
			if (parentPrimaryKey == null)
                throw new ArgumentNullException("parentPrimaryKey", Messages.DbRelation_ParentPkMayNotBeNull);

			if (childForeignKey == null)
                throw new ArgumentNullException("childForeignKey", Messages.DbRelation_ChildFkMayNotBeNull);

			if (parentPrimaryKey.Length != childForeignKey.Length)
                throw new InvalidOperationException(Messages.DbRelation_ParentAndChildKeysMustHaveEqualLen);

			for (int fieldIdxInRelation=0; fieldIdxInRelation<parentPrimaryKey.Length; fieldIdxInRelation++)
			{
                // throw new InvalidOperationException("Field types don't match: parentPrimaryKey[" + fieldIdxInRelation.ToString() + "].DbType != childForeignKey[" + fieldIdxInRelation.ToString() + "].DbType");
                if (parentPrimaryKey[fieldIdxInRelation].DbType != childForeignKey[fieldIdxInRelation].DbType)
                    throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, Messages.DbRelation_FieldTypesDontMatchXIsNotEqualToY, fieldIdxInRelation, fieldIdxInRelation));
			}
		}

		#endregion

		#region Properties.

		/// <summary>
		/// Gets the parent table.
		/// </summary>
		public IDbTable Parent
		{
			get {return this.parent;}
		}

		/// <summary>
		/// Gets the child table.
		/// </summary>
		public IDbTable Child
		{
			get {return this.child;}
		}

		/// <summary>
		/// Gets parent table's primary key fields.
		/// </summary>
		public IDbColumn[] ParentPrimaryKey
		{
			get {return this.parentPK;}
		}

		/// <summary>
		/// Gets the child table's foreign key fields which connects it to the parent table.
		/// </summary>
		public IDbColumn[] ChildForeignKey
		{
			get {return this.childFK;}
		}

		/// <summary>
		/// Gets or sets relation name.
		/// </summary>
		public string Name
		{
			get 
            {
                if (this.name == null)
                    this.name = GenerateName(this.parent, this.child);

                return this.name;
            }
			set {this.name = value;}
		}

		#endregion

		#region Methods.

		/// <summary>
		/// Checks whether the relation connects the given tables.
		/// </summary>
		/// <param name="first">1st table.</param>
		/// <param name="second">2nd table.</param>
		/// <returns>True if tables are connected with this relation, false otherwise.</returns>
		public bool Connects(IDbTable first, IDbTable second)
		{
			if ((first.HasEqualAliasAndNameAs(this.parent) && second.HasEqualAliasAndNameAs(this.child))
				||  (second.HasEqualAliasAndNameAs(this.parent)  &&  first.HasEqualAliasAndNameAs(this.child)))
			{
				return true;
			}
			else
			{
				return false;
			}
		}

		/// <summary>
		/// Checks whether the two relations use the same child FK fields.
		/// </summary>
		/// <param name="otherRelation">Other relation.</param>
		/// <returns>True if all FK fields have been matched; false otherwise.</returns>
		public bool HasEqualForeignKeyFieldsAs(DbRelation otherRelation)
		{	
			// Check if both relations use the same table as a child object.
			// Check only TableNames. Aliases may differ.
			if (this.child.TableName != otherRelation.child.TableName)
				return false;

			// FK count must be the same.
			if (this.childFK.Length != otherRelation.childFK.Length)
				return false;

			// Field order may be different, so all combinations must be tested.
			int matchedFkFieldsCount = 0;
			for (int i=0; i<this.childFK.Length; i++)
			{
				for (int j=0; j<otherRelation.childFK.Length; j++)
				{
					if (this.childFK[i].ColumnName ==  otherRelation.childFK[j].ColumnName)
					{
						matchedFkFieldsCount++;
						break;
					}
				}
			}

			bool allFkFieldsAreMatched = (matchedFkFieldsCount == this.childFK.Length);
			return allFkFieldsAreMatched;
		}

		#endregion

		#region ICloneable.

		/// <summary>
		/// Creates a deep copy clone of current instance.
		/// </summary>
		object ICloneable.Clone()
		{
			return Clone();
		}

		/// <summary>
		/// Creates a deep copy of current instance.
		/// </summary>
		public DbRelation Clone()
		{			
			DbRelation clone = new DbRelation();
			clone.CopyFrom(this);
			return clone;
		}

		private void CopyFrom(DbRelation source)
		{
			this.name = source.name;
			this.parent = source.parent.Clone(source.parent.Alias, source.parent.ColumnAliasesArePrefixed);
			//EnsureEqualColumnAliases(source.parent, this.parent);
			this.parentPK = GetColumns(this.parent, source.parentPK);
			this.child = source.child.Clone(source.child.Alias, source.child.ColumnAliasesArePrefixed);
			//EnsureEqualColumnAliases(this.child, source.child);
			this.childFK = GetColumns(this.child, source.childFK);
		}

        //private static void EnsureEqualColumnAliases(IDbTable source, IDbTable target)
        //{
        //    if (source.ColumnAliasesArePrefixed && !target.ColumnAliasesArePrefixed)
        //        target.SetPrefixedAliases();
        //}

		private static IDbColumn[] GetColumns(IDbTable table, IDbColumn[] columnsToGet)
		{
			IDbColumn[] cols = new IDbColumn[columnsToGet.Length];
			for (int idxCol = 0; idxCol < cols.Length; idxCol++)
			{
				string colName = columnsToGet[idxCol].ColumnName;
				cols[idxCol] = table.Columns.GetByColumnName(colName);
			}

			return cols;
		}

        /// <summary>
        /// Creates a deep copy of current instance.
        /// </summary>
        /// <param name="setPrefixedAliasesForChildColumns">Specifies whether child columns in cloned relation will have prefixed aliases. 
        /// This parameter overrides the value set in <see cref="IDbTable.ColumnAliasesArePrefixed"/> property.</param>
        /// <param name="setPrefixedAliasesForParentColumns">Specifies whether parent columns in cloned relation will have prefixed aliases. 
        /// This parameter overrides the value set in <see cref="IDbTable.ColumnAliasesArePrefixed"/> property.</param>
        public DbRelation Clone(bool setPrefixedAliasesForChildColumns, bool setPrefixedAliasesForParentColumns)
        {
            DbRelation clone = new DbRelation();
            clone.CopyFrom(this, null, setPrefixedAliasesForChildColumns, null, setPrefixedAliasesForParentColumns);
            return clone;
        }

        /// <summary>
        /// Creates a deep copy of current instance.
        /// </summary>
        /// <param name="childAlias">New child table alias. If <b>null</b> then current alias is used.</param>
        /// <param name="setPrefixedAliasesForChildColumns">Specifies whether child columns in cloned relation will have prefixed aliases. 
        /// This parameter overrides the value set in <see cref="IDbTable.ColumnAliasesArePrefixed"/> property.</param>
        /// <param name="parentAlias">New parent table alias. If <b>null</b> then current alias is used.</param>
        /// <param name="setPrefixedAliasesForParentColumns">Specifies whether parent columns in cloned relation will have prefixed aliases. 
        /// This parameter overrides the value set in <see cref="IDbTable.ColumnAliasesArePrefixed"/> property.</param>
        public DbRelation Clone(string childAlias, bool setPrefixedAliasesForChildColumns, string parentAlias, bool setPrefixedAliasesForParentColumns)
        {
            DbRelation clone = new DbRelation();
            clone.CopyFrom(this, childAlias, setPrefixedAliasesForChildColumns, parentAlias, setPrefixedAliasesForParentColumns);
            return clone;
        }

        private void CopyFrom(DbRelation source, string childAlias, bool setPrefixedAliasesForChildColumns, string parentAlias, bool setPrefixedAliasesForParentColumns)
        {
            this.name = source.name;
            this.child = source.child.Clone(childAlias ?? source.child.Alias, setPrefixedAliasesForChildColumns);
            this.childFK = GetColumns(this.child, source.childFK);
            this.parent = source.parent.Clone(parentAlias ?? source.parent.Alias, setPrefixedAliasesForParentColumns);
            this.parentPK = GetColumns(this.parent, source.parentPK);            
        }

		#endregion
	}
}
